/*
 * CoursesList.java
 *
 * Created on November 14, 2006, 1:06 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.courselist;

import edu.umn.cs5115.scheduler.*;
import edu.umn.cs5115.scheduler.entities.Course;
import edu.umn.cs5115.scheduler.entities.CoursePart;
import edu.umn.cs5115.scheduler.entities.MeetingTime;
import edu.umn.cs5115.scheduler.entities.Schedule;
import edu.umn.cs5115.scheduler.entities.Section;
import edu.umn.cs5115.scheduler.entities.Status;
import edu.umn.cs5115.scheduler.entities.Weekday;
import edu.umn.cs5115.scheduler.framework.DocumentController;
import edu.umn.cs5115.scheduler.framework.IteratorSafeSet;
import edu.umn.cs5115.scheduler.framework.KeyValueAdapter;
import edu.umn.cs5115.scheduler.framework.KeyValueCoding;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver.SetMutationType;
import edu.umn.cs5115.scheduler.framework.ListenerManager;
import edu.umn.cs5115.scheduler.framework.MenuAvailabilityListener;
import edu.umn.cs5115.scheduler.framework.Undoable;
import edu.umn.cs5115.scheduler.framework.gui.AlphaColor;
import edu.umn.cs5115.scheduler.framework.gui.Binder;
import edu.umn.cs5115.scheduler.framework.gui.Button;
import edu.umn.cs5115.scheduler.framework.gui.DisclosureTriangle;
import edu.umn.cs5115.scheduler.framework.gui.FakeCanvas;
import edu.umn.cs5115.scheduler.framework.gui.FakeWidget;
import edu.umn.cs5115.scheduler.framework.gui.Font;
import edu.umn.cs5115.scheduler.framework.gui.KeyValueCodingCheckbox;
import edu.umn.cs5115.scheduler.framework.gui.Label;
import edu.umn.cs5115.scheduler.framework.gui.MouseListener;
import edu.umn.cs5115.scheduler.framework.gui.SelectionController;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;

/**
 * This class contains the expandable list of courses on the left side of the 
 * interface.  This class provides the external interface for the CourseList.
 * In reality, it just packages another control to make it scrollable.
 * @author grant
 */
public class CoursesList extends Composite
{
    private CourseListInternal myCourseList;
    private ScrollBar scrollBar;
    private SchedulerDocument document;
    
    /**
     * Creates a new instance of CoursesList
     * @param parent The parent control for the CourseList.
     */
    public CoursesList(Composite parent, SchedulerDocument document)
    {
        super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL);
        this.document = document;
        myCourseList = new CourseListInternal(this, document);
        scrollBar = getVerticalBar();
        scrollBar.setIncrement(5);
        scrollBar.addListener(SWT.Selection, new Listener () {
            public void handleEvent (Event e) {
                repositionList();
            }
        });
        addListener(SWT.Resize, new Listener()
        {
            public void handleEvent(Event e)
            {
                Rectangle myArea = getClientArea();
                // the width may have changed, which may cause details to wrap,
                // changing the height.  We want to keep the thing at the top
                // in roughly the same position throughout this.
                Point oldSize = myCourseList.getSize();
                myCourseList.resizeToMinimumSize(myArea.width, myArea.height);
                Point newSize = myCourseList.getSize();
                int newSelection = 0;
                int oldSelection = scrollBar.getSelection();
                if (oldSize.y > 0)
                    newSelection = (int)(((double)oldSelection/oldSize.y)*newSize.y);
                scrollBar.setMaximum(newSize.y);
                scrollBar.setThumb(myArea.height);
                scrollBar.setPageIncrement((int)(myArea.height*.9)+1);
                scrollBar.setSelection(newSelection);
                repositionList();
            }
        });
        myCourseList.addListener(SWT.Resize, new Listener()
        {
            public void handleEvent(Event event)
            {
                // when the list resizes itself, we just want to change the 
                // maximum value for the scroller
                int newHeight = myCourseList.getSize().y;
                scrollBar.setMaximum(newHeight);
                // this may have caused the scroll bar's selection to change, so
                // make sure the list is in the correct location.
                repositionList();
            }
        });
    }
    
    private void repositionList()
    {
        Point location = myCourseList.getLocation();
        location.y = -scrollBar.getSelection();
        myCourseList.setLocation(location);
    }
    
    /** Adjusts the scrolling so that the given point is just visible.
     * Note:  implementation currently ignores the x coordinate.
     * @param x The x coordinate to force to be visible.
     * @param y The y coordinate to force to be visible.
     */
    private void scrollPointToVisible(int x, int y)
    {
        // ignores x coordinate for now...we don't do horizontal scrolling.
        Rectangle myArea = getClientArea();
        Point courseLocation = myCourseList.getLocation();
        x += courseLocation.x;
        y += courseLocation.y;
        if (y < 0)
        {
            courseLocation.y -= y;
        } else {
            if (y > myArea.height)
            {
                courseLocation.y -= (y - myArea.height);
            }
        }
        
        // Scroll to the correct point to get to the right location.
        scrollBar.setSelection(-courseLocation.y);
        // Also move the list
        myCourseList.setLocation(courseLocation);
    }
    
    /** Adjusts scrolling to try to make everything in a rectangle visible.
     * @param rectangle The rectangle to try to make just visible.
     */
    private void scrollRectangleToVisible(Rectangle rectangle)
    {
        scrollPointToVisible(rectangle.x+rectangle.width, rectangle.y+rectangle.height);
        scrollPointToVisible(rectangle.x, rectangle.y);
    }
    
    /**
     * The actual flat list of courses.
     */
    private static class CourseListInternal extends FakeCanvas
    {
        /** Space between courses. */
        private final static int INTER_COURSE_SPACE = 5;
        /** Margin between courses and the left/edge of the control. */
        private final static int HORIZONTAL_INSET = 3;
        /** margin between courses and the top/bottom edge of the control. */
        private final static int VERTICAL_INSET = 3;
        
        /** The background color to use.  Should be white. */
        private final static AlphaColor BACKGROUND_COLOR = AlphaColor.WHITE;
        
        /** Document this control displays courses from. */
        private SchedulerDocument document;
        
        /** List of courses blocks displayed in the control. */
        private final List<CourseBlock> courseList = new ArrayList();
        
        /** Sends notifications to MenuAvailabilityListeners for us. */
        private ListenerManager<MenuAvailabilityListener> listenerManager = new ListenerManager<MenuAvailabilityListener>(new ListenerManager.Notifier()
        {
            public void notifyListener(Object object)
            {
                ((MenuAvailabilityListener)object).availabilityChanged();
            }
        });
        
        /** Listens to changes in documents selection, and forwards that 
         * something has changed to our availability listeners. */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                listenerManager.notifyListeners();
            }
        };
        
        /** Listens to changes in children's size. */
        private KeyValueObserver resizeListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                repositionCourses();
            }
        };
        
        /** 
         * Temporarily stores states of removed courses, just before they are
         * re-added.  Because undo actions are performed in the reverse order,
         * this information is placed here immediately before a course is re-
         * added during undo. */
        private Map<Course, CourseListStateInformation> courseStates = new HashMap();
        
        /** Listens to changes in the document's course list. */
        private KeyValueObserver courseListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key, SetMutationType mutation, Set modifyingSet)
            {
                // TODO: figure out a way to get it so that undo will
                // restore the course's expanded state in the list.  Registering
                // undo actions here won't work directly, since they'd get 
                // executed before the course was recreated. (Undo actions are
                // performed in the reverse order of registration)
                switch (mutation)
                {
                    case UNION:
                        Iterator iterator = modifyingSet.iterator();
                        while(iterator.hasNext())
                        {
                            Course thisCourse = (Course) iterator.next();
                            createCourse(thisCourse);
                        }
                        repositionCourses();
                        break;
                    case MINUS:
                        iterator = modifyingSet.iterator();
                        while(iterator.hasNext())
                        {
                            final Course thisCourse = (Course) iterator.next();
                            Iterator<CourseBlock> blockIterator = courseList.iterator(); 
                            while(blockIterator.hasNext())
                            {
                                CourseBlock block = blockIterator.next();
                                if (block.getCourse() == thisCourse)
                                {
                                    final CourseListStateInformation blockState = new CourseListStateInformation(block, courseList.indexOf(block));
                                    document.getUndoManager().registerUndo(new Undoable()
                                    {
                                        public void undo()
                                        {
                                            courseStates.put(thisCourse, blockState);
                                        }
                                    });
                                    blockIterator.remove();
                                    remove(block);
                                    block.removeListener(resizeListener, FakeWidget.SIZE_KEY);
                                    break;
                                }
                            }
                            repositionCourses();
                        }
                        break;
                    // we don't bother to handle these cases gracefully, because
                    // in normal operation they never occur.
                    case INTERSECT:
                    case REPLACE:
                        recreateCourses();
                }
            }
        };
        
        /** Minimum height for the control. */
        int minimumHeight;
        /** Parent course list. */
        private CoursesList parent;
        
        public CourseListInternal(CoursesList parent, SchedulerDocument document)
        {
            super(parent, SWT.NO_BACKGROUND);
            this.parent = parent;
            this.document = document;
            document.getSchedule().addListener(courseListener, Schedule.COURSES_KEY);
            document.getSelectionController().addListener(selectionListener, SelectionController.SELECTION_KEY);
            recreateCourses();
        }

        public void resizeToMinimumSize(int width, int height)
        {
            int courseWidth = width - HORIZONTAL_INSET * 2;
            for(CourseBlock thisCourse : courseList)
            {
                thisCourse.resizeToWidth(courseWidth);
            }
            minimumHeight = height;
            height = getSize().y;
            if (height < minimumHeight)
                height = minimumHeight;
            setSize(width, height);
        }
        
        public void recreateCourses()
        {
            for(CourseBlock block : courseList)
            {
                remove(block);
                block.removeListener(resizeListener, FakeWidget.SIZE_KEY);
            }
            courseList.clear();
            Set courses = document.getSchedule().getCourses();
            Iterator iterator = courses.iterator();
            while(iterator.hasNext())
            {
                Course thisCourse = (Course) iterator.next();
                createCourse(thisCourse);
            }
            
            repositionCourses();
        }

        /**
         * Creates a single new course block to be displayed.  Note that this
         * does not lay out the new course, only adds it to the list.  After you
         * are done adding courses, call repositionCourses() to get them to
         * show up in the proper location.  In the future, this should be fixed
         * so that it's not the function user's responsibility.
         * @param thisCourse The course to create the new course block for.
         */
        private void createCourse(final Course thisCourse)
        {
            int width = getSize().x - HORIZONTAL_INSET * 2;
            CourseBlock newBlock = new CourseBlock(thisCourse);
            
            // this contains the expansion data for deleted courses that are
            // being re-added via undo.
            CourseListStateInformation stateInfo = courseStates.remove(thisCourse);
            if (stateInfo != null)
            {
                stateInfo.restoreBlock(newBlock);
                courseList.add(stateInfo.location, newBlock);
            } else {
                courseList.add(newBlock);
            }
            
            add(newBlock);
            newBlock.resizeToWidth(width);
            newBlock.addListener(resizeListener, FakeWidget.SIZE_KEY);
        }
        
        /**
         * Lays out all of the courses.
         */
        public void repositionCourses()
        {
            Point position = new Point(HORIZONTAL_INSET, VERTICAL_INSET);
            for(CourseBlock thisCourse : courseList)
            {
                thisCourse.setLocation(position);
                position.y += thisCourse.getHeight() + INTER_COURSE_SPACE;
            }
            
            if (position.y < minimumHeight)
                position.y = minimumHeight;
            setSize(getSize().x, position.y);
        }
        
        /**
         * Scroll a rectangle in the canvas to visible
         * @param rectangle The rectangle to make visible.
         */
        public void scrollRectangleToVisible(Rectangle rectangle)
        {
            parent.scrollRectangleToVisible(rectangle);
        }
        

        /**
         * Just clear the control with white.
         */
        public void paintControl(PaintEvent e)
        {
            BACKGROUND_COLOR.setBackground(e.gc);
            e.gc.fillRectangle(e.x, e.y, e.width, e.height);
        }
        
        public void mouseDown(MouseEvent e)
        {
            document.getSelectionController().setSelection(null);
        }
        
        /**
         * Implemented for use by the menu.  Called by the delete menu item when
         * it detects we are selected.
         */
        public void delete()
        {
            CoursePart selected = document.getSelectionController().getSelection();
            if (selected != null)
            {
                document.getSchedule().removeCourse(selected.getCourse());
                document.getUndoManager().setActionName("Delete Course");
            }
        }
        
        /**
         * Used by the menu to monitor the availability of menu items.  Currently
         * only delete is supported, so we ignore the action name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @param listener The listener to add to be notified when the 
         * availability of that menu item changes.
         */
        public void addAvailableListener(String actionName, MenuAvailabilityListener listener)
        {
            listenerManager.addListener(listener);
        }
        
        /**
         * Used by the menu to remove listeners on the availability of menu 
         * items.  Currently only delete is supported, so we ignore the action 
         * name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @param listener The listener to remove from being notified when the 
         * availability of that menu item changes.
         */
        public void removeAvailableListener(String actionName, MenuAvailabilityListener listener)
        {
            listenerManager.removeListener(listener);
        }
        
        /**
         * Used by the menu to check whether a given action is allowed, given
         * the object's current state.  Currently
         * only delete is supported, so we ignore the action name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @return True if the action can be performed now, false if it cannot 
         * be.
         */
        public boolean actionAvailable(String actionName)
        {
            CoursePart selected = document.getSelectionController().getSelection();
            return (selected != null);
        }
    }
    
    /**
     * The course block is the outer course block.  It draws the course header
     * information, and includes area for the children when they are visible.
     */
    private static class CourseBlock extends FakeWidget
    {
        /** The radius of the corners on the block. */
        private final static int CORNER_RADIUS = 5;
        /** Inset from the top-left corner of the control to the triangle. */
        private final static Point TRIANGLE_INSET = new Point(4, 4);
        /** Inset from the top-right corner of the control to the delete x. */
        private final static Point DELETE_INSET = new Point(4, 4);
        /** Horizontal spacing between the disclosure triangle and the text */
        private final static int TRIANGLE_TEXT_SPACE = 4;
        /** Horizontal spacing between the delete x and the text. */
        private final static int DELETE_TEXT_SPACE = 4;
        /** Space between the top of the control and the top of the text. */
        private final static int TOP_TEXT_INSET = 4;
        /** Space between the top of the control and the top of the status indicator. */
        private final static int TOP_STATUS_INSET = 7;
        /** Space between the right edge of the control and the text. */
        private final static int RIGHT_TEXT_INSET = 4;
        /** Space between the course text and the check mark. */
        private final static int TEXT_STATUS_SPACE = 4;
        /** Vertical Space between the course number and it's name. */
        private final static int COURSE_NUMBER_NAME_SPACE = 0;
        /** Space between the bottom of the course name and either the start of
         * the section list or the bottom of the control. */
        private final static int TEXT_BOTTOM_INSET = 4;
        /** Color of the outline. (drawn when course is selected.) */
        private final static AlphaColor OUTLINE_COLOR = new AlphaColor(0f,0f,0f,.3f);
        /** Width of selection ring */
        private final static int OUTLINE_WIDTH = 3;
        
        /** The course being displayed in this block. */
        private Course course;
        /** The list of section blocks in this block. */
        private List<SectionBlock> sections = new ArrayList();
        /** True if the course is selected, false otherwise. */
        private boolean selected;
        /** The height from the top of the control to the beginning of the list. */
        private int headerHeight;
        
        /** The color to use when drawing the course's color. */
        private AlphaColor courseColor;
        /** The color to use when drawing the course's contents color. */
        private AlphaColor contentsColor;
        /** The course's color (for use when selected.) */
        private AlphaColor selectedCourseColor;
        /** The course's content's color (for use when selected.) */
        private AlphaColor selectedContentsColor;
        /** The course's color (for use when not selected.) */
        private AlphaColor unselectedCourseColor;
        /** The course's contents color. (for use when not selected.) */
        private AlphaColor unselectedContentsColor;
        
        /** The disclosure triangle for this class. (visible only if there are children) */
        private DisclosureTriangle triangle;
        /** The widget for deleting this class. */
        private DeleteWidget deleter;
        /** The label containing the course's department/number */
        private Label courseNumber;
        /** The label containing the course's name. */
        private Label courseName;
        /** Displays the checkmark or "!" next to the course name. */
        private CourseStatusDisplayer statusDisplayer;
        
        /** Binds the course name text. */
        private Binder courseNameBinder;
        
        /** Binds the course status brief to the tool tip of this control itself. */
        private Binder toolTipBinder;
        
        /** Listens to selection to redraw us if we get selected. */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                CoursePart selection = ((SchedulerDocument)course.getDocument()).getSelectionController().getSelection();
                boolean selectedNow = (selection == course);
                if (selectedNow != selected)
                {
                    selected = selectedNow;
                    chooseCurrentColors();
                    redraw();
                }
                if (selectedNow)
                {
                    Rectangle bounds = getBounds();
                    Point actualTopLeft = getParent().convertPoint(new Point(bounds.x, bounds.y));
                    bounds.x = actualTopLeft.x;
                    bounds.y = actualTopLeft.y;
                    ((CourseListInternal)getCanvas()).scrollRectangleToVisible(bounds);
                }
                if (selection != course && selection != null && selection.getCourse() == course)
                    triangle.setExpanded(true);
            }
        };
        
        /** Listens for changes in course's color. */
        private KeyValueObserver colorListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                recalculateColors();
            }
        };
        
        /** Listens for the expansion of the triangle. */
        private KeyValueObserver expansionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                repositionSections();
            }
        };
        
        /** Listens for clicks to the delete "x". */
        private DeleteWidget.DeleteWidgetActionListener deleteListener = new DeleteWidget.DeleteWidgetActionListener()
        {
            public void actionPerformed(DeleteWidget widget)
            {
                SchedulerDocument document = ((SchedulerDocument)course.getDocument());
                document.getSchedule().removeCourse(course);
                document.getUndoManager().setActionName("Delete Course \"" + course.getShortName() + "\"");
            }
        };
        
        /** Listens for size changes in the children. */
        private KeyValueObserver resizeListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                repositionSections();
            }
        };
        
        /** Listens to changes in visibility of the status displayer to redo the
         * layout of the box when it appears or disappears. */
        private KeyValueObserver courseStatusVisibilityObserver = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                // redo the box's layout.
                resizeToWidth(getWidth());
            }
        };
        
        public CourseBlock(Course course)
        {
            this.course = course;
            course.addListener(colorListener, Course.COLOR_KEY);
            triangle = new DisclosureTriangle();
            add(triangle);
            deleter = new DeleteWidget();
            add(deleter);
            courseNumber = new Label();
            courseNumber.setFont(new Font(null, -1, SWT.BOLD));
            add(courseNumber);
            courseName = new Label();
            add(courseName);
            statusDisplayer = new CourseStatusDisplayer(course);
            add(statusDisplayer);
            
            courseName.setFont(new Font(null, 9, SWT.NORMAL));
            
            // bind the name
            courseNameBinder = new Binder(course, Course.NAME_KEY, courseName, Label.TEXT_KEY);
            
            // bind the course status brief to the tool tip
            toolTipBinder = new Binder(course, Course.STATUS_BRIEF_KEY, this, FakeWidget.TOOL_TIP_KEY);
            
            statusDisplayer.addListener(courseStatusVisibilityObserver, FakeWidget.HIDDEN_KEY);
            
            // for now, do this manually (it won't update...oh well)
            courseNumber.setText(course.getDepartment() + " " + course.getCourseNumber());
            
            // listen to document selection
            SelectionController<CoursePart> selectionController = ((SchedulerDocument)course.getDocument()).getSelectionController();
            selectionController.addListener(selectionListener, SelectionController.SELECTION_KEY);
            selected = (selectionController.getSelection() == course);
            
            // listen for triangle expansion
            triangle.addListener(expansionListener, DisclosureTriangle.EXPANDED_KEY);
            triangle.setExpanded(course.isAncestorOf(selectionController.getSelection()));
            
            deleter.addActionListener(deleteListener);
            
            recalculateColors();
            createSections();
        }

        protected void removeFromParent()
        {
            toolTipBinder.unbind();
            courseNameBinder.unbind();
            statusDisplayer.removeListener(courseStatusVisibilityObserver, FakeWidget.HIDDEN_KEY);
            remove(statusDisplayer);
            ((SchedulerDocument)course.getDocument()).getSelectionController().removeListener(selectionListener, SelectionController.SELECTION_KEY);
            triangle.removeListener(expansionListener, DisclosureTriangle.EXPANDED_KEY);
            remove(triangle);
            deleter.removeActionListener(deleteListener);
            remove(deleter);
            for(SectionBlock block : sections)
            {
                block.removeListener(resizeListener, FakeWidget.SIZE_KEY);
                remove(block);
            }
        }
        
        /** Create this course's sections. */
        private void createSections()
        {
            Set sectionsSet = course.getSections();
            Iterator iterator = sectionsSet.iterator();
            while(iterator.hasNext())
            {
                Section thisSection = (Section) iterator.next();
                SectionBlock newBlock = new SectionBlock(thisSection, 0);
                add(newBlock);
                sections.add(newBlock);
                newBlock.addListener(resizeListener, FakeWidget.SIZE_KEY);
            }
            
            repositionSections();
            
            if (sections.isEmpty())
                triangle.setHidden(true);
        }
        
        /** Get the list of child sections.
         * @return The list of children sections.  Do not modify this list.
         */
        public List<SectionBlock> getChildrenSections()
        {
            return sections;
        }
        
        /** Get whether or not the block is expanded.
         * @return True if the block is expanded, false if it is collapsed.
         */
        public boolean getExpanded()
        {
            return triangle.getExpanded();
        }
        
        /** Set whether or not the block is expanded.
         * @param b True if the block should be expanded, false if it should be
         * collapsed. */
        public void setExpanded(boolean b)
        {
            triangle.setExpanded(b);
        }
        
        /** Lays out all sections. */
        private void repositionSections()
        {
            if (triangle.getExpanded() && sections.size() > 0)
            {
                Point position = new Point(0, headerHeight);
                for(SectionBlock block : sections)
                {
                    block.setHidden(false);
                    block.setLocation(position);
                    position.y += block.getHeight();
                }
                position.y += CORNER_RADIUS;
                setSize(getWidth(), position.y);
            } else {
                for(SectionBlock block : sections)
                    block.setHidden(true);
                setSize(getWidth(), headerHeight);
            }
        }
        
        /**
         * Resizes this control to the specified width, possibly changing its
         * height.
         * @param width The width to resize the control to.
         */
        public void resizeToWidth(int width)
        {
            int neededHeight = TOP_TEXT_INSET;
            triangle.setLocation(TRIANGLE_INSET);
            deleter.setLocation(width-deleter.getWidth()-DELETE_INSET.x, DELETE_INSET.y);
            int left;
            if (!triangle.isHidden())
                left = triangle.getX() + triangle.getWidth() + TRIANGLE_TEXT_SPACE;
            else
                left = TRIANGLE_INSET.x;
            int right = deleter.getX() - DELETE_TEXT_SPACE;
            courseNumber.setLocation(left, TOP_TEXT_INSET);
            int usedWidth = courseNumber.resizeToWidth(right-left) + TEXT_STATUS_SPACE;
            if (!statusDisplayer.isHidden())
            {
                if (left + usedWidth + statusDisplayer.getWidth() > right)
                {
                    statusDisplayer.setLocation(left, TOP_TEXT_INSET+courseNumber.getHeight());
                    neededHeight += statusDisplayer.getHeight();
                } else
                    statusDisplayer.setLocation(left + usedWidth, TOP_STATUS_INSET);
            }
            neededHeight += courseNumber.getHeight() + COURSE_NUMBER_NAME_SPACE;
            right = width - RIGHT_TEXT_INSET;
            courseName.setLocation(left, neededHeight);
            courseName.resizeToWidth(right-left);
            neededHeight+=courseName.getHeight() + TEXT_BOTTOM_INSET;
            headerHeight = neededHeight;
            
            triangle.setLocation(TRIANGLE_INSET.x, (neededHeight - triangle.getHeight())/2);
            
            setSize(width, neededHeight);
            // resize children here
            for(SectionBlock block : sections)
                block.resizeToWidth(width);
            repositionSections();
        }

        /** Fetches the color from the course again, and recalculates our colors. */
        private void recalculateColors()
        {
            selectedCourseColor = course.getColor();
            selectedContentsColor = selectedCourseColor.blendWithColor(.75f, AlphaColor.WHITE);
            unselectedCourseColor = selectedCourseColor.blendWithColor(.2f, AlphaColor.WHITE);
            unselectedContentsColor = selectedContentsColor.blendWithColor(.2f, AlphaColor.WHITE);
            chooseCurrentColors();
        }
        
        /** Sets the proper colors to use for drawing. */
        private void chooseCurrentColors()
        {
            if (selected)
            {
                courseColor = selectedCourseColor;
                contentsColor = selectedContentsColor;
            } else {
                courseColor = unselectedCourseColor;
                contentsColor = unselectedContentsColor;
            }
        }
        
        public void paintComponent(GC g)
        {
            courseColor.setBackground(g);
            g.fillRoundRectangle(0, 0, getWidth(), getHeight(), 2*CORNER_RADIUS, 2*CORNER_RADIUS);
            
            
            if (headerHeight < getHeight())
            {
                // draw the course list background underneath the header.
                contentsColor.setBackground(g);
                g.fillRectangle(0, headerHeight, getWidth(), getHeight()-headerHeight-CORNER_RADIUS);
            }
            
            if (selected)
            {
                OUTLINE_COLOR.setForeground(g);
                g.setLineWidth(OUTLINE_WIDTH);
                int inset = OUTLINE_WIDTH/2;
                g.drawRoundRectangle(inset,inset, getWidth()-OUTLINE_WIDTH, getHeight()-OUTLINE_WIDTH, CORNER_RADIUS*2, CORNER_RADIUS*2);
            }
        }
        
        public boolean mouseDown(MouseEvent e)
        {
            ((SchedulerDocument)course.getDocument()).getSelectionController().setSelection(course);
            return true;
        }

        public boolean mouseDoubleClick(MouseEvent e)
        {
            triangle.toggle();
            return true;
        }

        /**
         * Get the course for this section.
         * @return This section's course.
         */
        public Course getCourse()
        {
            return course;
        }
    }
    
    /**
     * Rows in the course list.
     */
    private static class SectionBlock extends FakeWidget
    {
        /** Extra Amount to indent at every level. */
        private final static int INDENT_PER_LEVEL = 18;
        /** Horizontal space from edge of indent to left edge of checkbox. */
        private final static int CHECKBOX_INSET = 4;
        /** Space from edge of checkbox to triangle. */
        private final static int CHECKBOX_TRIANGLE_SPACING = 4;
        /** Space from edge of triangle to text. */
        private final static int TRIANGLE_TEXT_SPACING = 4;
        /** Space from top of control to the text. */
        private final static int TOP_TEXT_INSET = 4;
        /** Space from bottom of control to text. */
        private final static int BOTTOM_TEXT_INSET = 4;
        /** Space between lecture information and location. */
        private final static int TEXT_TEXT_SPACING = 0;
        /** Font for the primary info label */
        private final static Font PRIMARY_INFO_FONT = new Font(null, 12, SWT.NORMAL);
        /** Font for the secondary info label */
        private final static Font SECONDARY_INFO_FONT = new Font(null, 9, SWT.NORMAL);
        
        /** List of child sections. */
        private List<SectionBlock> sections = new ArrayList();
        
        /** Section represented by this SectionBlock. */
        private Section section;
        /** Triangle control */
        private DisclosureTriangle triangle;
        /** Checkbox. */
        private KeyValueCodingCheckbox checkbox;
        /** Location of checkbox, relative to us.  We use this to reposition the
         * checkbox whenever we're drawn, since if some of our ancestors move,
         * it may not be positioned in the correct absolute location any more.
         * (since it's a real widget, not a fake one, and won't move 
         * automatically with us)
         */
        private Point checkBoxLocation = new Point(4, 4);
        /** provides highlighting for our background. */
        private BackgroundHighlighter backgroundHighlighter;
        /** Space occupied by the row for only this section, not its children. */
        private int myHeight;
        /** Label with main information. ("Lecture; 8:00-8:50 MW") */
        private Label primaryInfoLabel;
        /** Label with secondary information. ("SciCB 125") */
        private Label secondaryInfoLabel;
        /** Number of levels to indent. */
        int indentLevel;
        
        // binds the checkbox to the section's chosen attribute
        private Binder checkboxBinder;
        
        /** Listen to our own visibility and update the checkboxes. */
        private KeyValueObserver hideListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                if (checkbox != null)
                    checkbox.setHidden(isHidden());
                // hide children, too.
                if (triangle.getExpanded())
                {
                    for(SectionBlock block : sections)
                        block.setHidden(isHidden());
                }
                
                // Necessary if the sections have never been positioned before.
                // This is possible if the course was deleted and re-added via
                // undo when sections underneath the course were expanded, but
                // the course itself was collapsed.
                if (!isHidden())
                    repositionSections();
            }
        };
        
        /** Moves the checkbox whenever we move. */
        private KeyValueObserver moveListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                Point converted = convertPoint(checkBoxLocation);
                if (checkbox != null)
                    checkbox.setLocation(converted.x, converted.y);
            }
        };
        
        /** Listens for checkbox expansion. */
        private KeyValueObserver expansionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                repositionSections();
            }
        };
        
        /** Listens to selection and expand if one of our children gets selected. */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                CoursePart selection = ((SchedulerDocument)section.getDocument()).getSelectionController().getSelection();
                if (section.isAncestorOf(selection))
                    triangle.setExpanded(true);
                if (selection == section)
                {
                    Rectangle bounds = getBounds();
                    Point actualTopLeft = getParent().convertPoint(new Point(bounds.x, bounds.y));
                    bounds.x = actualTopLeft.x;
                    bounds.y = actualTopLeft.y;
                    ((CourseListInternal)getCanvas()).scrollRectangleToVisible(bounds);
                }
            }
        };
        
        /**
         * Creates a new section block.
         * @param section The section this block will represent.
         * @param level The indentation level for this section.
         */
        public SectionBlock(Section section, int level)
        {
            this.section = section;
            indentLevel = level;
            backgroundHighlighter = new BackgroundHighlighter(section);
            add(backgroundHighlighter);
            triangle = new DisclosureTriangle();
            add(triangle);
            addListener(hideListener, FakeWidget.HIDDEN_KEY);
            primaryInfoLabel = new Label();
            primaryInfoLabel.setFont(PRIMARY_INFO_FONT);
            secondaryInfoLabel = new Label();
            secondaryInfoLabel.setFont(SECONDARY_INFO_FONT);
            add(primaryInfoLabel);
            add(secondaryInfoLabel);
            
            MeetingTime meetingTime = null;
            if (section.getMeetingTimes().size() > 0)
                meetingTime = (MeetingTime) section.getMeetingTimes().iterator().next();
            
            
            String primaryText = section.getHumanReadableType();
            if (meetingTime != null && meetingTime.getStartTime() != null && meetingTime.getEndTime() != null)
            {
                primaryText += "; ";
		SimpleDateFormat formatter = new SimpleDateFormat();
		formatter.applyPattern("h:mm");
                primaryText += formatter.format(meetingTime.getStartTime());
                primaryText += "-" + formatter.format(meetingTime.getEndTime()) + " ";
                for(Weekday weekday : meetingTime.getMeetingDays())
                    primaryText += weekday.getVeryAbbreviatedName();
            }
            
            primaryInfoLabel.setText(primaryText);
            
            String secondaryText = "";
            if (meetingTime != null)
                secondaryText = meetingTime.getLocation();
            
            secondaryInfoLabel.setText(secondaryText);
            
            SelectionController<CoursePart> selectionController = ((SchedulerDocument)section.getDocument()).getSelectionController();
            selectionController.addListener(selectionListener, SelectionController.SELECTION_KEY);
            // fake a selection change to get the triangle expanded if needed
            selectionListener.valueChanged(selectionController, SelectionController.SELECTION_KEY);
            
            recreateChildren();
            
            triangle.addListener(expansionListener, DisclosureTriangle.EXPANDED_KEY);
            
            addListener(moveListener, FakeWidget.LOCATION_IN_CANVAS_KEY);
        }

        protected void setParent(FakeWidget parent)
        {
            super.setParent(parent);
        }
        
        /** clean up stuff we made. */
        protected void removeFromParent()
        {
            if (checkbox != null)
            {
                checkboxBinder.unbind();
                checkbox.dispose();
            }
            removeListener(moveListener, FakeWidget.LOCATION_IN_CANVAS_KEY);
            triangle.removeListener(expansionListener, DisclosureTriangle.EXPANDED_KEY);
            
            remove(backgroundHighlighter);
            for(SectionBlock block : sections)
            {
                // TODO: add this back in once we get resize listener support
                // Add resize listener support!
                //block.removeListener(resizeListener, FakeWidget.SIZE_KEY);
                remove(block);
            }
            
            ((SchedulerDocument)section.getDocument()).getSelectionController().removeListener(selectionListener, SelectionController.SELECTION_KEY);
            
            super.removeFromParent();
        }
        
        /** Creates widgets for the children sections. */
        private void recreateChildren()
        {
            Collection sectionsCollection = section.getChildren();
            Iterator iterator = sectionsCollection.iterator();
            while(iterator.hasNext())
            {
                Section thisSection = (Section) iterator.next();
                SectionBlock newBlock = new SectionBlock(thisSection, indentLevel+1);
                add(newBlock);
                sections.add(newBlock);
            }
            
            repositionSections();
            
            if (sections.isEmpty())
                triangle.setHidden(true);
        }
        
        /** Adjust the position of all children sections. */
        private void repositionSections()
        {
            if (!isAnyParentHidden() && triangle.getExpanded() && sections.size() > 0)
            {
                Point position = new Point(0, myHeight);
                for(SectionBlock block : sections)
                {
                    block.setHidden(false);
                    block.setLocation(position);
                    position.y += block.getHeight();
                }
                setSize(getWidth(), position.y);
            } else {
                for(SectionBlock block : sections)
                    block.setHidden(true);
                setSize(getWidth(), myHeight);
            }
        }
        
        /** Get the list of child sections.
         * @return The list of children sections.  Do not modify this list.
         */
        public List<SectionBlock> getChildrenSections()
        {
            return sections;
        }
        
        /** Get whether or not the block is expanded.
         * @return True if the block is expanded, false if it is collapsed.
         */
        public boolean getExpanded()
        {
            return triangle.getExpanded();
        }
        
        /** Set whether or not the block is expanded.
         * @param b True if the block should be expanded, false if it should be
         * collapsed. */
        public void setExpanded(boolean b)
        {
            triangle.setExpanded(b);
        }
        
        /**
         * Recalculates position information given the width and resizes the
         * control.
         */
        public void resizeToWidth(int width)
        {
            int textHeight = TOP_TEXT_INSET;
            int indent = INDENT_PER_LEVEL * indentLevel;
            if (checkbox == null)
            {
                checkbox = new KeyValueCodingCheckbox(getCanvas());
                checkbox.setHidden(isHidden());
                checkboxBinder = new Binder(section, Section.CHOSEN_KEY, checkbox, KeyValueCodingCheckbox.SELECTION_KEY);
            }
            Point checkboxSize = checkbox.getSize();
            int left = indent+CHECKBOX_INSET + checkboxSize.x + TRIANGLE_TEXT_SPACING;
            if (!triangle.isHidden())
                left += CHECKBOX_TRIANGLE_SPACING + triangle.getWidth();
            primaryInfoLabel.setLocation(left, textHeight);
            primaryInfoLabel.resizeToWidth(width-left);
            textHeight += primaryInfoLabel.getHeight();
            
            if (secondaryInfoLabel.getText() != "")
            {
                textHeight += TEXT_TEXT_SPACING;
                secondaryInfoLabel.setLocation(left, textHeight);
                secondaryInfoLabel.resizeToWidth(width-left);
                textHeight += secondaryInfoLabel.getHeight();
            }
            
            textHeight += BOTTOM_TEXT_INSET;
            
            checkBoxLocation.x = CHECKBOX_INSET + indent;
            checkBoxLocation.y = (textHeight-checkboxSize.y)/2;
            Point location = convertPoint(checkBoxLocation);
            checkbox.setLocation(location.x, location.y);
            left = indent+ CHECKBOX_INSET + checkbox.getSize().x + CHECKBOX_TRIANGLE_SPACING;
            triangle.setLocation(left, (textHeight-triangle.getHeight())/2);
            left += triangle.getWidth() + TRIANGLE_TEXT_SPACING;
            
            myHeight = textHeight;
            backgroundHighlighter.setBounds(0, 0, width, myHeight);
            
            // now, ask children to resize, too
            for(SectionBlock section : sections)
                section.resizeToWidth(width);
            
            setSize(width, myHeight);
            
            repositionSections();
        }
        
        public boolean mouseDown(MouseEvent e)
        {
            ((SchedulerDocument)section.getDocument()).getSelectionController().setSelection(section);
            return true;
        }

        public boolean mouseDoubleClick(MouseEvent e)
        {
            triangle.toggle();
            return true;
        }

        /** Overriden only to move the checkbox.  We don't actually do any of
         * our own drawing. */
        public void paintComponent(GC g)
        {
            Point location = convertPoint(checkBoxLocation);
            checkbox.setLocation(location.x, location.y);
        }
    }
    
    /**
     * Paints backgrounds for CourseSections.  The reason this is a separate 
     * widget is because otherwise we can't get mouse enter/exit events for just
     * the row that we're interested in.
     */
    private static class BackgroundHighlighter extends FakeWidget
    {
        /** True when this item is selected. */
        private boolean selected;
        /** True when the mouse is over this item. */
        private boolean inside;
        /** Background color for when the item is selected. */
        private AlphaColor selectedColor;
        /** Background color for when the mouse is over the selected item. */
        private AlphaColor mouseOverColor;
        /** Section to highlight the background of. */
        private Section section;
        
        /** Listen for mouse enter/exit events to update the background. */
        private MouseListener mouseListener = new MouseListener()
        {
            public void mouseEntered(MouseEvent e)
            {
                inside = true;
                redraw();
            }
            public void mouseExited(MouseEvent e)
            {
                inside = false;
                redraw();
            }
        };
        
        /** Listen to the selection and update our color */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                boolean shouldSelect = ((SchedulerDocument)section.getDocument()).getSelectionController().getSelection() == section;
                if (shouldSelect != selected)
                {
                    selected = shouldSelect;
                    redraw();
                }
            }
        };
        
        public BackgroundHighlighter(Section section)
        {
            this.section = section;
            
            addMouseListener(mouseListener);
            recreateColors();
            SelectionController<CoursePart> selectionController = ((SchedulerDocument)section.getDocument()).getSelectionController();
            selectionController.addListener(selectionListener, SelectionController.SELECTION_KEY);
            // fake a value change to get the intial values correct.
            selectionListener.valueChanged(selectionController, SelectionController.SELECTION_KEY);
        }

        protected void removeFromParent()
        {
            removeMouseListener(mouseListener);
            ((SchedulerDocument)section.getDocument()).getSelectionController().removeListener(selectionListener, SelectionController.SELECTION_KEY);
            
            super.removeFromParent();
        }
        
        /** Recreates the colors for selection, etc. */
        private void recreateColors()
        {
            selectedColor = section.getCourse().getColor();
            mouseOverColor = new AlphaColor(selectedColor.getRed(), selectedColor.getGreen(), selectedColor.getBlue(), 32);
            selectedColor = new AlphaColor(selectedColor.getRed(), selectedColor.getGreen(), selectedColor.getBlue(), 64);
        }

        public void paintComponent(GC g)
        {
            if (selected)
            {
                selectedColor.setBackground(g);
                g.fillRectangle(0, 0, bounds.width, bounds.height);
            } 
            if (inside) 
            {
                mouseOverColor.setBackground(g);
                g.fillRectangle(0, 0, bounds.width, bounds.height);
            }
        }

        /** Let clicks fall through this widget. */
        public boolean mouseDoubleClick(MouseEvent e) { return false; }
        /** Let clicks fall through this widget. */
        public boolean mouseDown(MouseEvent e) { return false; }
        /** Let clicks fall through this widget. */
        public boolean mouseUp(MouseEvent e) { return false; }
    }
    
    /**
     * The delete "X".
     */
    private static class DeleteWidget extends FakeWidget
    {
        private final static int X_SIZE = 8;
        private final static Point SHADOW_OFFSET = new Point(1, 1);
        private final static int LINE_WIDTH = 2;
        private final static AlphaColor NORMAL_COLOR = new AlphaColor(106, 106, 106, 128);
        private final static AlphaColor MOUSE_OVER_COLOR = new AlphaColor(106, 106, 106);
        private final static AlphaColor PRESSED_COLOR = new AlphaColor(60, 60, 60);
        private final static AlphaColor NORMAL_SHADOW_COLOR = new AlphaColor(255, 255, 255, 32);
        private final static AlphaColor MOUSE_OVER_SHADOW_COLOR = new AlphaColor(255, 255, 255, 64);
        private final static AlphaColor PRESSED_SHADOW_COLOR = new AlphaColor(200, 200, 200, 64);
        
        /** Listeners */
        private IteratorSafeSet<DeleteWidgetActionListener> listeners = new IteratorSafeSet();
        
        /** Tracks whether or not the mouse is inside the widget. */
        private boolean inside;
        /** Tracks whether or not the mouse is pressed. */
        private boolean mouseIsDown;
        
        /** Listens for mouse entry/exit events to change the display. */
        private MouseListener mouseListener = new MouseListener()
        {
            public void mouseEntered(MouseEvent e)
            {
                inside = true;
                redraw();
            }
            public void mouseExited(MouseEvent e)
            {
                inside = false;
                redraw();
            }
        };
        
        /**
         * Add a listener to receive notifications when the x is clicked.
         * @param listener the listener to add.
         */
        public void addActionListener(DeleteWidgetActionListener listener)
        {
            listeners.add(listener);
        }
        
        /**
         * Remove a listener so that it no longer receives notifications of the
         * x being clicked.
         * @param listener the listener to remove.
         */
        public void removeActionListener(DeleteWidgetActionListener listener)
        {
            listeners.remove(listener);
        }
        
        /** Notifies all listeners of the delete action. */
        private void notifyListeners()
        {
            listeners.startIterating();
            for(DeleteWidgetActionListener listener : listeners)
                listener.actionPerformed(this);
            listeners.finishIterating();
        }
        
        public DeleteWidget()
        {
            setToolTip("Delete course");
            setSize(X_SIZE+SHADOW_OFFSET.x, X_SIZE+SHADOW_OFFSET.y);
            addMouseListener(mouseListener);
        }

        public boolean mouseDown(MouseEvent e)
        {
            mouseIsDown = true;
            redraw();
            return true;
        }
        
        public boolean mouseUp(MouseEvent e)
        {
            mouseIsDown = false;
            if (inside)
                notifyListeners();
            redraw();
            return true;
        }
        
        /** draw the x. */
        public void paintComponent(GC g)
        {
            g.setLineWidth(LINE_WIDTH);
            
            AlphaColor color = NORMAL_COLOR;
            AlphaColor shadowColor = NORMAL_SHADOW_COLOR;
            if (mouseIsDown || inside)
            {
                color = MOUSE_OVER_COLOR;
                shadowColor = MOUSE_OVER_SHADOW_COLOR;
            }
            if (mouseIsDown && inside)
            {
                color = PRESSED_COLOR;
                shadowColor = PRESSED_SHADOW_COLOR;
                
                shadowColor.setForeground(g);
                g.drawLine(0, 0, X_SIZE, X_SIZE);
                g.drawLine(0, X_SIZE, X_SIZE, 0);
                
                color.setForeground(g);

                g.drawLine(SHADOW_OFFSET.x, SHADOW_OFFSET.y, X_SIZE+SHADOW_OFFSET.x, X_SIZE+SHADOW_OFFSET.y);
                g.drawLine(SHADOW_OFFSET.x, SHADOW_OFFSET.y+X_SIZE, SHADOW_OFFSET.x+X_SIZE, SHADOW_OFFSET.y);
            } else {
                shadowColor.setForeground(g);

                g.drawLine(SHADOW_OFFSET.x, SHADOW_OFFSET.y, X_SIZE+SHADOW_OFFSET.x, X_SIZE+SHADOW_OFFSET.y);
                g.drawLine(SHADOW_OFFSET.x, SHADOW_OFFSET.y+X_SIZE, SHADOW_OFFSET.x+X_SIZE, SHADOW_OFFSET.y);

                color.setForeground(g);

                g.drawLine(0, 0, X_SIZE, X_SIZE);
                g.drawLine(0, X_SIZE, X_SIZE, 0);
            }
        }
        
        /** Interface that can receive notifications of when the x is clicked. */
        public static interface DeleteWidgetActionListener
        {
            /** Indicates that the delete widget was pressed. 
             * @param widget The widget that was clicked.
             */
            public void actionPerformed(DeleteWidget widget);
        }
    }
    
    /** Displays the complete checkmark or the problem exclamation point. */
    private static class CourseStatusDisplayer extends Button
    {
        /** Key for the status. */
        public static String STATUS_KEY = "status";
        
        private Status status;
        
        private static Button.ImageSet completedImages;
        private static Button.ImageSet problemImages;
        
        static
        {
            Display display = DocumentController.getSharedDocumentController().getDisplay();
            try
            {
                Image normalCompleted = new Image(display, CoursesList.class.getClass().getResource("/edu/umn/cs5115/scheduler/resources/complete-normal.png").openStream());
                Image normalProblem = new Image(display, CoursesList.class.getClass().getResource("/edu/umn/cs5115/scheduler/resources/problem-normal.png").openStream());
                Image overProblem = new Image(display, CoursesList.class.getClass().getResource("/edu/umn/cs5115/scheduler/resources/problem-over.png").openStream());
                Image downProblem = new Image(display, CoursesList.class.getClass().getResource("/edu/umn/cs5115/scheduler/resources/problem-down.png").openStream());
                completedImages = new Button.ImageSet(
                        normalCompleted,
                        normalCompleted,
                        normalCompleted);
                problemImages = new Button.ImageSet(
                        normalProblem,
                        overProblem,
                        downProblem);
            } catch (IOException e) {
                DocumentController.getSharedDocumentController().displayApplicationError("Unable to load a resource file.  Some graphics will not be properly displayed.");
                e.printStackTrace();
            }
        }
        
        /** Binds the status to the course part's status. */
        private Binder statusBinder;
        /** Binds the status brief to the tool tip. */
        private Binder toolTipBinder;
        
        /** Course part we're displaying the status for. */
        private CoursePart myPart;
        
        /** Listens for clicks on ourself and selects the deepest child with
         * a problem when there is a click. */
        private ButtonActionListener actionListener = new Button.ButtonActionListener()
        {
            public void actionPerformed(Button button)
            {
                if (getImageSet() == problemImages)
                {
                    // find the first deepest child with the problem.
                    Status problem = myPart.getStatus();
                    CoursePart currentPart = myPart;
                    Collection<CoursePart> children = currentPart.getChildren();
                    while(!children.isEmpty())
                    {
                        CoursePart nextPart = null;
                        for(CoursePart child : children)
                        {
                            if (child.getStatus() == problem)
                            {
                                // continue search on this child - there may be
                                // others, but we'll work on resolving this one
                                // first.
                                nextPart = child;
                                break;
                            }
                        }
                        
                        // did we find a child with the problem?
                        if (nextPart != null)
                        {
                            currentPart = nextPart;
                        } else {
                            // No?  Then break out of the loop to select the currentPart.
                            break;
                        }
                        children = currentPart.getChildren();
                    }
                    
                    ((SchedulerDocument)myPart.getDocument()).getSelectionController().setSelection(currentPart);
                }
            }
        };
        
        /**
         * Creates a new status displayer for the given CoursePart.  The status
         * will be automatically bound.
         * @param CoursePart The course part to create the displayer for.
         */
        public CourseStatusDisplayer(CoursePart part)
        {
            myPart = part;
            statusBinder = new Binder(part, CoursePart.STATUS_KEY, this, STATUS_KEY);
            toolTipBinder = new Binder(part, CoursePart.STATUS_BRIEF_KEY, this, TOOL_TIP_KEY);
            addActionListener(actionListener);
        }
        
        protected void removeFromParent()
        {
            statusBinder.unbind();
            toolTipBinder.unbind();
            removeActionListener(actionListener);
            super.removeFromParent();
        }
        
        /**
         * Get the status currently being displayed by the button.
         * @return The status being displayed.
         */
        public Status getStatus()
        {
            return status;
        }
        
        /**
         * Set the status currently being displayed by the button.
         * @param status The new status to be displayed.
         */
        public void setStatus(Status status)
        {
            if (this.status != status)
            {
                this.status = status;
                switch (status)
                {
                    case COMPLETE:
                        setImageSet(completedImages);
                        setHidden(false);
                        break;
                    case CONFLICT_MISSING:
                    case CONFLICT_MULTIPLE:
                    case CONFLICT_TIME:
                        setImageSet(problemImages);
                        setHidden(false);
                        break;
                    case NONE:
                        setHidden(true);
                }
                didChangeValueForKey(STATUS_KEY);
            }
        }
    
        /** Allow double clicks to fall through to the parent. */
        public boolean mouseDoubleClick(MouseEvent e)
        {
            return false;
        }
        
        /** Only pass this off to super if we're in a "problem" status. 
         * Otherwise allow click through.
         */
        public boolean mouseDown(MouseEvent e)
        {
            if (getImageSet() == problemImages)
                return super.mouseDown(e);
            return false;
        }
        /** Only pass this off to super if we're in a "problem" status. 
         * Otherwise allow click through.
         */
        public boolean mouseUp(MouseEvent e)
        {
            if (getImageSet() == problemImages)
                return super.mouseUp(e);
            return false;
        }
    }
    
    /** Stores some course state information. */
    private static class CourseListStateInformation
    {
        /** Location of the course in the list. */
        public int location;
        
        /** Stores expansion information of course. */
        private CourseState state;
        
        /**
         * Create a new CourseListStateInformation to expand a given course block.
         * @param original The block to copy the expansion state of.
         * @param location The location (index) of the course block in the 
         * course list.
         */
        public CourseListStateInformation(CourseBlock original, int location)
        {
            state = new CourseState(original);
            this.location = location;
        }
        
        /** Sets the expansion state of a course group to the expansion state
         * of the group this state information was created from.
         * @param block The block to copy the stored state information into.
         */
        public void restoreBlock(CourseBlock block)
        {
            state.restoreBlock(block);
        }
        
        /** Base class for SectionState and CourseState. */
        private static abstract class ExpansionList
        {
            protected boolean blockExtended;
            protected List<ExpansionList> children;
            
            /** Create a new expansion list.  The list's information is drawn
             * from the given object, using the getChildren and getState 
             * methods.
             */
            protected ExpansionList(Object o)
            {
                blockExtended = getState(o);
                List<SectionBlock> children = getChildren(o);
                this.children = new ArrayList(children.size());
                for(SectionBlock section : children)
                    this.children.add(new SectionState(section));
            }
            
            /** Expands the block and its children to their original state.
             * @param block The block to expand from.  It should be the same as
             * the block the list was created with, except for the lost 
             * expansion information.
             */
            protected void restoreBlock(Object o)
            {
                setState(o, blockExtended);
                Iterator<SectionBlock> restoredChildren = getChildren(o).iterator();
                Iterator<ExpansionList> restorers = children.iterator();
                while(restoredChildren.hasNext() && restorers.hasNext())
                    restorers.next().restoreBlock(restoredChildren.next());
            }
            
            /** Get the children for the given object.
             * @param o The object to fetch the children from.
             */
            protected abstract List<SectionBlock> getChildren(Object o);
            
            /** Get the expansion state of this object.
             * @param o The object to get the expnasion state of.
             * @return True if the object is expanded, false otherwise.
             */
            protected abstract boolean getState(Object o);
            
            /** Set the expansion state of this object.
             * @param o The object to set the expansion state of.
             * @param b The value of the expansion state; true to expand, false
             * to collpase.
             */
            protected abstract void setState(Object o, boolean b);
        }   
        
        private static class SectionState extends ExpansionList
        {
            /** Create a new SectionState to expand a certain block.
             * @param original The block to expand.
             */
            public SectionState(SectionBlock original)
            {
                super(original);
            }
            
            /** Expands the block and its children to their original state.
             * @param block The block to expand from.  It should be the same as
             * the block the list was created with, except for the lost 
             * expansion information.
             */
            public void restoreBlock(SectionBlock block)
            {
                super.restoreBlock(block);
            }

            protected List<CoursesList.SectionBlock> getChildren(Object o)
            {
                return ((SectionBlock)o).getChildrenSections();
            }

            protected boolean getState(Object o)
            {
                return ((SectionBlock)o).getExpanded();
            }

            protected void setState(Object o, boolean b)
            {
                ((SectionBlock)o).setExpanded(b);
            }
        }
        
        private static class CourseState extends ExpansionList
        {
            /** Create a new CourseState to expand a certain block.
             * @param original The block to expand.
             */
            public CourseState(CourseBlock original)
            {
                super(original);
            }
            
            /** Expands the block and its children to their original state.
             * @param block The block to expand from.  It should be the same as
             * the block the list was created with, except for the lost 
             * expansion information.
             */
            public void restoreBlock(CourseBlock block)
            {
                super.restoreBlock(block);
            }

            protected List<CoursesList.SectionBlock> getChildren(Object o)
            {
                return ((CourseBlock)o).getChildrenSections();
            }

            protected boolean getState(Object o)
            {
                return ((CourseBlock)o).getExpanded();
            }

            protected void setState(Object o, boolean b)
            {
                ((CourseBlock)o).setExpanded(b);
            }
            
        }
    }
}
